﻿// -----------------------------------------------------------------------
//    This file is part of TSGE.
//
//    TSGE is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 2 of the License, or
//    (at your option) any later version.
//
//    TSGE is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with TSGE.  If not, see <http://www.gnu.org/licenses/>.
// -----------------------------------------------------------------------

namespace tsge.Classes
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Security.Cryptography;
    using System.Text;
    using System.Windows;
    using System.Windows.Media;

    /// <summary>
    /// Terraria Singleton Class Implementation
    /// 
    /// </summary>
    public class Terraria
    {
        /// <summary>
        /// Internal singleton instance of this class.
        /// </summary>
        private static Terraria m_vInstance = null;

        /// <summary>
        /// Internal buff list storage.
        /// </summary>
        private List<Buff> m_vBuffList = new List<Buff>();

        /// <summary>
        /// Internal item list storage.
        /// </summary>
        private List<Item> m_vItemList = new List<Item>();

        /// <summary>
        /// Internal item prefix list storage.
        /// </summary>
        private List<ItemPrefix> m_vItemPrefixList = new List<ItemPrefix>();

        /// <summary>
        /// Internal hair file list.
        /// </summary>
        private List<String> m_vHairFiles = new List<String>();

        /// <summary>
        /// Private Constructor
        /// </summary>
        private Terraria() { }

        /// <summary>
        /// Gets the single instance of this class.
        /// </summary>
        public static Terraria Instance
        {
            get { return (m_vInstance != null) ? m_vInstance : m_vInstance = new Terraria(); }
        }

        /// <summary>
        /// Game version this program supports.
        /// </summary>
        public static Int32 GAME_VERSION = 39; // 1.1.2

        /// <summary>
        /// Default profile path to Terraria's save game files.
        /// </summary>
        public static String PROFILE_PATH = String.Join(
            Path.DirectorySeparatorChar.ToString(),
            new String[]
        {
            Environment.GetFolderPath(Environment.SpecialFolder.Personal),
            "My Games",
            "Terraria",
            "Players",
        });

        /// <summary>
        /// Encryption key used to encrypt and decrypt player files.
        /// </summary>
        private static String ENCRYPTION_KEY = "h3y_gUyZ";

        /// <summary>
        /// Encryption key converted to byte data for crypto provider.
        /// </summary>
        private static byte[] ENCRYPTION_DATA = UnicodeEncoding.Unicode.GetBytes(ENCRYPTION_KEY);

#if DEBUG
        /// <summary>
        /// Decrypts all player files inside the player profile path.
        /// </summary>
        public void DecryptAllPlayers()
        {
            IEnumerable<String> strFiles = Directory.EnumerateFiles(Terraria.PROFILE_PATH, "*.plr");
            foreach (String strFile in strFiles)
            {
                byte[] btData = this.DecryptFile(strFile);
                String file = new FileInfo(strFile).Name;

                FileStream fStream = new FileStream(Terraria.PROFILE_PATH + "\\dumped\\" + file, FileMode.CreateNew, FileAccess.Write);
                fStream.Write(btData, 0, btData.Length);
                fStream.Close();
            }
        }
#endif

        /// <summary>
        /// Attempts to load a player profile file.
        /// </summary>
        /// <param name="strPlayerFile">Player file to load.</param>
        /// <returns></returns>
        public Player LoadPlayer(String strPlayerFile)
        {
            // Attempt to decrypt the player file..
            byte[] btPlayerData = this.DecryptFile(strPlayerFile);
            if (btPlayerData == null) return default(Player);

            // Attempt to read the player file..
            using (MemoryStream mStream = new MemoryStream(btPlayerData))
            {
                using (BinaryReader bReader = new BinaryReader(mStream))
                {
                    Player p = new Player();

                    // Ensure game version is valid..
                    p.GameVersion = bReader.ReadInt32();
                    if (p.GameVersion != Terraria.GAME_VERSION)
                    {
                        throw new InvalidDataException("GameVersion does not match the supported value!");
                    }

                    // Read basic player info..
                    p.Name = bReader.ReadString();
                    p.Difficulty = bReader.ReadByte();
                    p.Hair = bReader.ReadInt32();
                    p.IsMale = bReader.ReadBoolean();
                    p.Health = bReader.ReadInt32();
                    p.HealthMax = bReader.ReadInt32();
                    p.Mana = bReader.ReadInt32();
                    p.ManaMax = bReader.ReadInt32();

                    // Read character colors..
                    p.HairColor = new Color().FromBytes(bReader.ReadBytes(3));
                    p.SkinColor = new Color().FromBytes(bReader.ReadBytes(3));
                    p.EyeColor = new Color().FromBytes(bReader.ReadBytes(3));
                    p.ShirtColor = new Color().FromBytes(bReader.ReadBytes(3));
                    p.UnderShirtColor = new Color().FromBytes(bReader.ReadBytes(3));
                    p.PantsColor = new Color().FromBytes(bReader.ReadBytes(3));
                    p.ShoesColor = new Color().FromBytes(bReader.ReadBytes(3));

                    // Read Armor..
                    for (int x = 0; x < 3; x++)
                    {
                        p.Armor[x].SetItem(bReader.ReadInt32());
                        p.Armor[x].Prefix = bReader.ReadByte();
                    }

                    // Read accessories..
                    for (int x = 0; x < 5; x++)
                    {
                        p.Accessories[x].SetItem(bReader.ReadInt32());
                        p.Accessories[x].Prefix = bReader.ReadByte();
                    }

                    // Read vanity items..
                    for (int x = 0; x < 3; x++)
                    {
                        p.Vanity[x].SetItem(bReader.ReadInt32());
                        p.Vanity[x].Prefix = bReader.ReadByte();
                    }

                    // Read Inventory..
                    for (int x = 0; x < 48; x++)
                    {
                        p.Inventory[x].SetItem(bReader.ReadInt32());
                        p.Inventory[x].Count = bReader.ReadInt32();
                        p.Inventory[x].Prefix = bReader.ReadByte();
                    }

                    // Read Bank1..
                    for (int x = 0; x < 20; x++)
                    {
                        p.Bank1[x].SetItem(bReader.ReadInt32());
                        p.Bank1[x].Count = bReader.ReadInt32();
                        p.Bank1[x].Prefix = bReader.ReadByte();
                    }

                    // Read Bank2..
                    for (int x = 0; x < 20; x++)
                    {
                        p.Bank2[x].SetItem(bReader.ReadInt32());
                        p.Bank2[x].Count = bReader.ReadInt32();
                        p.Bank2[x].Prefix = bReader.ReadByte();
                    }

                    // Read Buffs..
                    for (int x = 0; x < 10; x++)
                    {
                        p.Buffs[x].SetBuff(bReader.ReadInt32());
                        p.Buffs[x].Duration = bReader.ReadInt32();
                    }

                    // Read Server Entries..
                    for (int x = 0; x < 200; x++)
                    {
                        int nTemp = bReader.ReadInt32();
                        if (nTemp == -1) break;

                        p.ServerEntries[x].SpawnX = nTemp;
                        p.ServerEntries[x].SpawnY = bReader.ReadInt32();
                        p.ServerEntries[x].ServerAddress = bReader.ReadInt32();
                        p.ServerEntries[x].ServerName = bReader.ReadString();
                    }

                    // Read Hotbar Locked Flag..
                    p.IsHotbarLocked = bReader.ReadBoolean();

                    return p;
                }
            }
        }

        /// <summary>
        /// Attempts to save a player object to the given profile file.
        /// </summary>
        /// <param name="strFilePath">Player profile file to save to.</param>
        /// <param name="p">Player object to save.</param>
        /// <returns></returns>
        public bool SavePlayer(String strFilePath, Player p)
        {
            FileStream fStream = null;
            BinaryWriter bWriter = null;

            try
            {
                // Open and create a temp file to write to..
                fStream = new FileStream("tsge.dat", FileMode.OpenOrCreate, FileAccess.Write);
                bWriter = new BinaryWriter(fStream);

                // Write game version..
                bWriter.Write(p.GameVersion);

                // Write basic player info..
                bWriter.Write(p.Name);
                bWriter.Write(p.Difficulty);
                bWriter.Write(p.Hair);
                bWriter.Write(p.IsMale);
                bWriter.Write(p.Health);
                bWriter.Write(p.HealthMax);
                bWriter.Write(p.Mana);
                bWriter.Write(p.ManaMax);

                // Write player colors..
                bWriter.Write(p.HairColor.ToBytes());
                bWriter.Write(p.SkinColor.ToBytes());
                bWriter.Write(p.EyeColor.ToBytes());
                bWriter.Write(p.ShirtColor.ToBytes());
                bWriter.Write(p.UnderShirtColor.ToBytes());
                bWriter.Write(p.PantsColor.ToBytes());
                bWriter.Write(p.ShoesColor.ToBytes());

                // Write player armor..
                for (int x = 0; x < p.Armor.Length; x++)
                {
                    bWriter.Write(p.Armor[x].NetID);
                    bWriter.Write(p.Armor[x].Prefix);
                }

                // Write player accessories..
                for (int x = 0; x < p.Accessories.Length; x++)
                {
                    bWriter.Write(p.Accessories[x].NetID);
                    bWriter.Write(p.Accessories[x].Prefix);
                }

                // Write player vanity items..
                for (int x = 0; x < p.Vanity.Length; x++)
                {
                    bWriter.Write(p.Vanity[x].NetID);
                    bWriter.Write(p.Vanity[x].Prefix);
                }

                // Write player inventory..
                for (int x = 0; x < p.Inventory.Length; x++)
                {
                    bWriter.Write(p.Inventory[x].NetID);
                    bWriter.Write(p.Inventory[x].Count);
                    bWriter.Write(p.Inventory[x].Prefix);
                }

                // Write player bank1..
                for (int x = 0; x < p.Bank1.Length; x++)
                {
                    bWriter.Write(p.Bank1[x].NetID);
                    bWriter.Write(p.Bank1[x].Count);
                    bWriter.Write(p.Bank1[x].Prefix);
                }

                // Write player bank2..
                for (int x = 0; x < p.Bank2.Length; x++)
                {
                    bWriter.Write(p.Bank2[x].NetID);
                    bWriter.Write(p.Bank2[x].Count);
                    bWriter.Write(p.Bank2[x].Prefix);
                }

                // Write player buffs..
                for (int x = 0; x < p.Buffs.Length; x++)
                {
                    bWriter.Write(p.Buffs[x].Id);
                    bWriter.Write(p.Buffs[x].Duration);
                }

                // Write player server history..
                for (int x = 0; x < p.ServerEntries.Length; x++)
                {
                    if (p.ServerEntries[x].SpawnX == -1)
                    {
                        bWriter.Write(p.ServerEntries[x].SpawnX);
                        break;
                    }

                    bWriter.Write(p.ServerEntries[x].SpawnX);
                    bWriter.Write(p.ServerEntries[x].SpawnY);
                    bWriter.Write(p.ServerEntries[x].ServerAddress);
                    bWriter.Write(p.ServerEntries[x].ServerName);
                }

                // Write player hotbar locked flag..
                bWriter.Write(p.IsHotbarLocked);

                // Cleanup open stream objects..
                bWriter.Close();
                bWriter = null;
                fStream.Close();
                fStream = null;

                // Encrypt the file and save it to the real path..
                bool bReturn = this.EncryptFile("tsge.dat", strFilePath);

                // Delete temp copy of this profile..
                File.Delete("tsge.dat");
                
                return bReturn;
            }
            finally
            {
                // Ensure streams are cleaned up..
                if (bWriter != null) { bWriter.Close(); }
                if (fStream != null) { fStream.Close(); }
            }
        }

        /// <summary>
        /// Decrypts the given player file and returns the data as a byte array.
        /// </summary>
        /// <param name="strFileName">Profile file to decrypt.</param>
        /// <returns>Byte array of the decrypted file.</returns>
        private byte[] DecryptFile(String strFileName)
        {
            FileStream fStream = null;
            MemoryStream mStream = null;
            CryptoStream cStream = null;

            try
            {
                // Open and prepare the file for decryption..
                RijndaelManaged rjm = new RijndaelManaged();
                fStream = new FileStream(strFileName, FileMode.Open, FileAccess.Read);
                cStream = new CryptoStream(fStream, rjm.CreateDecryptor(ENCRYPTION_DATA, ENCRYPTION_DATA), CryptoStreamMode.Read);
                mStream = new MemoryStream();

                // Decrypt the file..
                int nRead = -1;
                while ((nRead = cStream.ReadByte()) != -1)
                {
                    mStream.WriteByte((byte)nRead);
                }

                // Return the decrypted file data..
                return mStream.GetBuffer().Clone() as byte[];
            }
            catch (Exception ex)
            {
                MessageBox.Show("Exception caught while trying to decrypt a player profile:\r\n\r\n" + ex.ToString(), "Exception Caught", MessageBoxButton.OK, MessageBoxImage.Error);
                return default(byte[]);
            }
            finally
            {
                // Ensure streams are cleaned up..
                if (cStream != null) { cStream.Close(); }
                if (mStream != null) { mStream.Close(); }
                if (fStream != null) { fStream.Close(); }
            }
        }

        /// <summary>
        /// Encrypts the temp file and saves it to the given profile file.
        /// </summary>
        /// <param name="strTempFile">Temp file to encrypt.</param>
        /// <param name="strFileName">Player profile to save the encrypted file to.</param>
        /// <returns>Boolean if successful.</returns>
        private bool EncryptFile(String strTempFile, String strFileName)
        {
            FileStream fStreamTmp = null;
            FileStream fStreamOut = null;
            CryptoStream cStream = null;

            try
            {
                // Open and prepare the file for decryption..
                RijndaelManaged rjm = new RijndaelManaged();
                fStreamOut = new FileStream(strFileName, FileMode.Create, FileAccess.Write);
                cStream = new CryptoStream(fStreamOut, rjm.CreateEncryptor(ENCRYPTION_DATA, ENCRYPTION_DATA), CryptoStreamMode.Write);
                fStreamTmp = new FileStream(strTempFile, FileMode.Open, FileAccess.Read);

                // Encrypt the file..
                Int32 nRead = -1;
                while ((nRead = fStreamTmp.ReadByte()) != -1)
                {
                    cStream.WriteByte((byte)nRead);
                }

                // Cleanup the open streams..
                fStreamTmp.Close();
                cStream.Close();
                fStreamOut.Close();

                return true;
            }
            catch (Exception ex)
            {
                MessageBox.Show("Exception caught while trying to save profile file: \r\n\r\n", ex.ToString(), MessageBoxButton.OK, MessageBoxImage.Error);
                return false;
            }
            finally
            {
                // Ensure streams are cleaned up..
                if (cStream != null) { cStream.Close(); }
                if (fStreamTmp != null) { fStreamTmp.Close(); }
                if (fStreamOut != null) { fStreamOut.Close(); }
            }
        }

        /// <summary>
        /// Gets or sets the list of buffs.
        /// </summary>
        public List<Buff> Buffs
        {
            get
            {
                if (this.m_vBuffList != null && this.m_vBuffList.Count > 0)
                    this.m_vBuffList.Sort(new tsge.Helpers.NaturalBuffNameComparer());
                return this.m_vBuffList;
            }
            set { this.m_vBuffList = value; }
        }

        /// <summary>
        /// Gets or sets the list of items.
        /// </summary>
        public List<Item> Items
        {
            get
            {
                if (this.m_vItemList != null && this.m_vItemList.Count > 0)
                    this.m_vItemList.Sort(new tsge.Helpers.NaturalItemNameComparer());
                return this.m_vItemList;
            }
            set { this.m_vItemList = value; }
        }

        /// <summary>
        /// Gets or sets the list of item prefixes.
        /// </summary>
        public List<ItemPrefix> Prefixes
        {
            get
            {
                if (this.m_vItemPrefixList != null && this.m_vItemPrefixList.Count > 0)
                    this.m_vItemPrefixList.Sort(new tsge.Helpers.NaturalItemPrefixNameComparer());
                return this.m_vItemPrefixList;
            }
            set { this.m_vItemPrefixList = value; }
        }

        /// <summary>
        /// Gets or sets the list of hair files.
        /// </summary>
        public List<String> HairFiles
        {
            get { return this.m_vHairFiles; }
            set { this.m_vHairFiles = value; }
        }
    }
}
